package org.cs.rebind;

import java.io.PrintWriter;

import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.config.TypedStringValue;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.io.ClassPathResource;

import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;


public class BeanFactoryGenerator extends Generator {
	private String className = null;
	private String packageName = null;
	private String typeName = null;	
	private GenericApplicationContext springContext;
	
	@Override
	public String generate(TreeLogger logger, GeneratorContext context,
			String typeName) throws UnableToCompleteException {
			
		
		//this is to avoid to regenerate the java files for every permutations 
		if (typeName.equals(this.typeName) && springContext != null) 
			return packageName + "." + className;
		
		this.typeName = typeName;		
		TypeOracle typeOracle = context.getTypeOracle();
		
		try {
			JClassType classType = typeOracle.getType(typeName);
			packageName = classType.getPackage().getName();
			className = classType.getSimpleSourceName() + "_BeanFactory";
			
			//load the spring bean definitions
			this.loadSpringBeanFactory(typeName);
			
			// Generate source code
			generateClass(logger, context);

		} catch (Exception e) {
			logger.log(TreeLogger.ERROR, "BeanFactoryGenerator failed to generate BeanFactory", e);
			throw new UnableToCompleteException();
		}
		return packageName + "." + className;

	}

	private void generateClass(TreeLogger logger, GeneratorContext context) throws UnableToCompleteException {

		// get print writer
		PrintWriter printWriter = null;
		printWriter = context.tryCreate(logger, packageName, className);
		if (printWriter == null) //source already generated
			return;

		// init composer, set class properties, create source writer
		ClassSourceFileComposerFactory composer = null;
		composer = new ClassSourceFileComposerFactory(packageName, className);
		composer.setSuperclass("org.cs.client.generator.BeanFactoryImpl");
		SourceWriter sourceWriter = null;
		sourceWriter = composer.createSourceWriter(context, printWriter);
		
		// generator constructor source code
		generateConstructor(sourceWriter);
		
		//generate createBean method
		sourceWriter.println("protected Object createBean(String beanId) {");
		sourceWriter.indent();

		//generate the createBean for each bean defined in xml
		String[] beanNames = this.springContext.getBeanDefinitionNames();
		for (int i=0; i<beanNames.length; i++) {
			BeanDefinition bd = this.springContext.getBeanDefinition(beanNames[i]);
			this.generateBeanBuildBlock(logger, beanNames[i], bd, context, sourceWriter);
		}
		
		sourceWriter.println("return null;");
		sourceWriter.outdent();
		sourceWriter.println("}");
		
		// close out
		sourceWriter.outdent();
		sourceWriter.println("}");

		// commit
		context.commit(logger, printWriter);
	}

	/**
	 * Generate source code for the default constructor. 
	 * 
	 * @param sourceWriter
	 *            Source writer to output source code
	 */
	private void generateConstructor(SourceWriter sourceWriter) {
		// start
		sourceWriter.println("public " + className + "() { ");
		sourceWriter.indent();
		sourceWriter.println("super();");

		// end 
		sourceWriter.outdent();
		sourceWriter.println("}");
	}
	
	/**
	 * Generate source code for bean building block 
	 * 
	 * @param logger 
	 *     TreeLogger passed into the method for message logging
	 * @param id 
	 *     bean id as defined in the xml file (name)
	 * @param beanDef 
	 *     Spring BeanDefinition as defined in the xml file 
	 * @param context 
	 *     GeneratorContext passed by GWT
	 * @param sw
	 *     SourceWriter passed into the method for source code generation   
	 */
	private void generateBeanBuildBlock(TreeLogger logger, String id, BeanDefinition beanDef, GeneratorContext context, SourceWriter sw) throws UnableToCompleteException {
		//if
		sw.println("if (\""+id+"\""+".equals(beanId)) {");
		sw.indent();
				
		TypeOracle typeOracle = context.getTypeOracle();
		JClassType beanType = typeOracle.findType(beanDef.getBeanClassName());

		//instantiate
		sw.println(beanDef.getBeanClassName()+" bean = new "+beanDef.getBeanClassName()+"();");
		String singletonInd = "false";
		if (beanDef.isSingleton()) singletonInd = "true";
		sw.println("addBean(\""+id+"\", bean, "+singletonInd+");");

		//bean set ups
		PropertyValue[] pValues = beanDef.getPropertyValues().getPropertyValues();
		for (int i=0; i<pValues.length; i++) {
			PropertyValue pv = pValues[i];
			Object val = pv.getValue();
			if (val instanceof RuntimeBeanReference) { //bean reference
				RuntimeBeanReference beanRefValue  = (RuntimeBeanReference) val;
				String refBeanName = beanRefValue.getBeanName();
				BeanDefinition refBeanDef = this.springContext.getBeanDefinition(refBeanName);
				String refBeanClassName = refBeanDef.getBeanClassName();
				sw.println("{");
				sw.println(refBeanClassName+ " refBean = ("+ refBeanClassName+") getBean(\""+refBeanName+"\");");  
				sw.println("bean.set"+this.prepareMethodName(pv.getName())+"(refBean);");
				sw.println("}");
			}
			else if (val instanceof TypedStringValue) { //value
				TypedStringValue stringValue = (TypedStringValue) val;
				String value = stringValue.getValue();
				sw.println(this.buildValueSetter(logger, beanType, beanDef.getBeanClassName(), pv.getName(), value));
				
			}
			
		}

		//return and end of if
		sw.println("return bean;");
		sw.outdent();
		sw.println("}");
	}

	/**
	 * Load the Spring bean factory definition xml file
	 * 
	 * @param factoryName 
	 *     Name of the factory 
	 */
	private void loadSpringBeanFactory(String factoryName) throws Exception {
		String resourceName = factoryName.replace('.', '/') + ".xml";
		
		springContext = new GenericApplicationContext();
		XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader(springContext);
		xmlReader.loadBeanDefinitions(new ClassPathResource(resourceName));

	}
	
	private String prepareMethodName(String name) {
		return name.substring(0, 1).toUpperCase()+name.substring(1, name.length());
	}
	
	private String buildValueSetter(TreeLogger logger, JClassType beanType, String beanClass, String propertyName, String value) throws UnableToCompleteException {
		
		
		if (beanType == null) {
			logger.log(TreeLogger.ERROR, "Class defintion not found: "+beanClass, null);
			throw new UnableToCompleteException();
		}
		
		String setterName = "set"+this.prepareMethodName(propertyName);
		
		JMethod[] methods = beanType.getMethods();
		String quote = "";
		for (int i=0; i<methods.length; i++) { 
			JMethod method = methods[i];
			if (method.getName().equals(setterName)) {
				JType type = method.getParameters()[0].getType();
				if (type instanceof JPrimitiveType) {
					JPrimitiveType pt = (JPrimitiveType) type;
					if (pt.getQualifiedSourceName().equals("char")) quote = "'"; 
				}
				else if (type.getQualifiedSourceName().equals("Character")) quote = "'";
				else { // we assume this is String
					quote = "\"";
				}
				String stmt = "bean."+method.getName()+"("+quote+value+quote+");";
				return stmt;
			}
		}
		String stmt = null;
		if (beanType.getSuperclass() != null)
			stmt = this.buildValueSetter(logger, beanType.getSuperclass(), beanClass, propertyName, value);
		
		if (stmt != null) return stmt;
		
		logger.log(TreeLogger.ERROR, "No setter on "+beanClass+ " for property "+propertyName, null);
		throw new UnableToCompleteException();		
	}
}